﻿/*
 * KLF Headless Browser
 * Written by Kris Oye <kristianoye@gmail.com>
 * Copyright (C) 2011-2012.  All rights reserved.
 */
namespace KLF.Lib.Browser.DOM
{
    #region Using Statements

    using Jint;
    using System;
    using System.Linq;
    using System.Collections.Generic;
    using KLF.Lib.Browser.DOM.Interface;
    using KLF.Lib.Browser.DOM.HTML;
    using KLF.Lib.Browser.DOM.View;

    #endregion

    public class Document : Node, IDocumentView
    {
        #region Constructors

        public Document()
        {
            Owner = this;
            __state = DocumentState.Uninitialized;
        }

        #endregion

        #region Document State

        /// <summary>
        /// Indicates what state the underlying document is in.
        /// </summary>
        internal DocumentState __state;

        internal virtual DocumentState _state
        {
            get { return __state; }
            set { __state = value; }
        }

        public virtual string readyState
        {
            get
            {
                return __state.ToString().ToLower();
            }
        }

        #endregion

        #region DOM Level 1 Compliance

        public override Node cloneNode(bool deep)
        {
            throw new System.NotImplementedException();
        }

        /// <summary>
        /// Creates an Attr of the given name. Note that the Attr instance 
        /// can then be set on an Element using the setAttribute method.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public virtual Attr createAttribute(string name)
        {
            return new Attr(this, name);
        }

        /// <summary>
        /// Creates a CDATASection node whose value is the specified string. 
        /// </summary>
        /// <param name="data">The data for the CDATASection contents.</param>
        /// <returns>The new CDATASection object.</returns>
        public virtual CDATASection createCDATASection(string data)
        {
            return new CDATASection(this, data);
        }

        /// <summary>
        /// Creates a Comment node given the specified string. 
        /// </summary>
        /// <param name="data">The data for the node.</param>
        /// <returns>The new Comment object.</returns>
        public virtual Comment createComment(string data)
        {
            return new Comment(this, data);
        }

        /// <summary>
        /// Creates an empty DocumentFragment object. 
        /// </summary>
        /// <returns>A new DocumentFragment.</returns>
        public virtual DocumentFragment createDocumentFragment()
        {
            return new DocumentFragment(this);
        }

        /// <summary>
        /// Creates an element of the type specified. Note that the instance 
        /// returned implements the Element interface, so attributes can be 
        /// specified directly on the returned object.
        /// </summary>
        /// 
        /// <param name="tagName">The name of the element type to instantiate.
        /// For XML, this is case-sensitive. For HTML, the tagName parameter 
        /// may be provided in any case, but it must be mapped to the canonical
        /// uppercase form by the DOM implementation. </param>
        /// 
        /// <returns>A new Element object.</returns>
        public virtual Element createElement(string tagName)
        {
            return new Element(this, tagName);
        }

        /// <summary>
        /// Creates an EntityReference object. In addition, if the referenced
        /// entity is known, the child list of the EntityReference node is 
        /// made the same as that of the corresponding Entity node.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public virtual EntityReference createEntityReference(string name)
        {
            return new EntityReference(this, name);
        }

        /// <summary>
        /// Creates a ProcessingInstruction node given the specified name and data strings. 
        /// </summary>
        /// <param name="target">The target part of the processing instruction.</param>
        /// <param name="data">The data for the node.</param>
        /// <returns></returns>
        public virtual ProcessingInstruction createProcessingInstruction(string target, string data)
        {
            return new ProcessingInstruction(this, target, data);
        }

        /// <summary>
        /// Creates a Text node given the specified string.
        /// </summary>
        /// <param name="text">The data for the node.</param>
        /// <returns>The new Text object.</returns>
        public virtual Text createTextNode(string text)
        {
            return new Text(this, text);
        }

        /// <summary>
        /// This is a convenience attribute that allows direct access to the
        /// child node that is the root element of the document. For HTML 
        /// documents, this is the element with the tagName "HTML"
        /// </summary>
        public virtual Element documentElement
        {
            get;
            internal set;
        }

        internal DocumentType _DocumentType;

        /// <summary>
        /// The Document Type Declaration (see DocumentType) associated with
        /// this document. For HTML documents as well as XML documents 
        /// without a document type declaration this returns null. The DOM 
        /// Level 1 does not support editing the Document Type Declaration, 
        /// therefore docType cannot be altered in any way.
        /// </summary>
        public DocumentType doctype
        {
            get
            {
                return _DocumentType;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="tagName"></param>
        /// <returns></returns>
        public virtual NodeList getElementsByTagName(string tagName)
        {
            var result = Elements.Where(e => ((Element)e).TagName.Equals(tagName) || tagName.Equals("*"));

            return new NodeList(result.ToArray());
        }

        /// <summary>
        /// The DOMImplementation object that handles this document. A DOM
        /// application may use objects from multiple implementations.
        /// </summary>
        public virtual DOMImplementation implementation
        {
            get
            {
                return new DOMImplementation();
            }
        }

        public override string nodeName
        {
            get { return "#document"; }
        }

        public override string nodeValue
        {
            get { return null; }
        }

        public override byte nodeType
        {
            get { return DOCUMENT_NODE; }
        }

        #endregion

        #region DOM Level 2 Compliance

        /// <summary>
        /// Creates an attribute of the given qualified name and namespace URI.
        /// 
        /// Per [XML Namespaces], applications must use the value null as the 
        /// namespaceURI parameter for methods if they wish to have no 
        /// namespace.
        /// </summary>
        /// <param name="namespaceURI"></param>
        /// <param name="qualifiedName"></param>
        /// <returns></returns>
        public Attr createAttributeNS(string namespaceURI, string qualifiedName)
        {
            return new Attr(this, qualifiedName)
            {
                namespaceURI = namespaceURI,
                prefix = (qualifiedName.Contains(':') ?
                    qualifiedName.Substring(0, qualifiedName.IndexOf(':')) :
                    null),
                localName = (qualifiedName.Contains(':') ?
                    qualifiedName.Substring(qualifiedName.IndexOf(':') + 1) :
                    qualifiedName),
                Value = String.Empty
            };
        }

        /// <summary>
        /// Creates an element of the given qualified name and namespace URI.
        /// 
        /// Per [XML Namespaces], applications must use the value null as the
        /// namespaceURI parameter for methods if they wish to have no namespace.
        /// </summary>
        /// <param name="namespaceURI"></param>
        /// <param name="qualifiedName"></param>
        /// <returns></returns>
        public Element createElementNS(string namespaceURI, string qualifiedName)
        {
            return new Element(this, qualifiedName)
            {
                namespaceURI = namespaceURI,
                prefix = (qualifiedName.Contains(':') ? 
                    qualifiedName.Substring(0, qualifiedName.IndexOf(':')) : 
                    null),
                localName = (qualifiedName.Contains(':') ? 
                    qualifiedName.Substring(qualifiedName.IndexOf(':') + 1) :
                    qualifiedName)
            };
        }

        /// <summary>
        /// Returns the Element that has an ID attribute with the given value.
        /// If no such element exists, this returns null. If more than one 
        /// element has an ID attribute with that value, what is returned 
        /// is undefined.
        /// 
        /// The DOM implementation is expected to use the attribute 
        /// Attr.isId to determine if an attribute is of type ID. 
        /// </summary>
        /// <param name="elementID"></param>
        /// <returns></returns>
        public Element getElementById(string elementID)
        {
            var result = Elements.Where(e =>
                !String.IsNullOrEmpty(e.getAttribute(HTMLConstants.HTML_ATT_ID)) &&
                e.getAttribute(HTMLConstants.HTML_ATT_ID).Equals(elementID));

            return (result.Count() == 1 ? result.First() : null);
        }

        /// <summary>
        /// Returns a NodeList of all the Elements with a given local name 
        /// and namespace URI in document order.
        /// </summary>
        /// <param name="namespaceURI"></param>
        /// <param name="localName"></param>
        /// <returns></returns>
        public NodeList getElementsByTagNameNS(string namespaceURI, string localName)
        {
            var result = Elements.Where(e =>
                (e.namespaceURI.Equals(namespaceURI) || namespaceURI.Equals("*") &&
                (e.localName.Equals(localName) || localName.Equals("*"))));

            return new NodeList(result.ToArray());
        }

        /// <summary>
        /// Imports a node from another document to this document, without
        /// altering or removing the source node from the original document;
        /// this method creates a new copy of the source node. The returned 
        /// node has no parent; (parentNode is null).
        /// 
        /// For all nodes, importing a node creates a node object owned by 
        /// the importing document, with attribute values identical to the 
        /// source node's nodeName and nodeType, plus the attributes related 
        /// to namespaces (prefix, localName, and namespaceURI). As in the 
        /// cloneNode operation, the source node is not altered. User data 
        /// associated to the imported node is not carried over. However, if 
        /// any UserDataHandlers has been specified along with the associated 
        /// data these handlers will be called with the appropriate parameters
        /// before this method returns.
        /// 
        /// Additional information is copied as appropriate to the nodeType,
        /// attempting to mirror the behavior expected if a fragment of XML 
        /// or HTML source was copied from one document to another, 
        /// recognizing that the two documents may have different DTDs in the 
        /// XML case. The following list describes the specifics for each 
        /// type of node. 
        /// </summary>
        /// <param name="importedNode"></param>
        /// <param name="deep"></param>
        /// <returns></returns>
        public Node importNode(Node importedNode, bool deep)
        {
            Node import = null;

            switch (importedNode.nodeType)
            {
                case DOCUMENT_NODE:
                case DOCUMENT_TYPE_NODE:
                    throw new DOMException(DOMException.NOT_SUPPORTED_ERR);

                case ATTRIBUTE_NODE:
                case DOCUMENT_FRAGMENT_NODE:
                case ELEMENT_NODE:
                case ENTITY_NODE:
                case ENTITY_REFERENCE_NODE:
                case NOTATION_NODE:
                case PROCESSING_INSTRUCTION_NODE:
                case TEXT_NODE: 
                case CDATA_SECTION_NODE:
                case COMMENT_NODE:
                    import = importedNode.cloneNode(deep);
                    break;

            }

            return import;
        }

        #endregion

        #region DOM Level 3 Compliance

        /// <summary>
        /// Attempts to adopt a node from another document to this document.
        /// If supported, it changes the ownerDocument of the source node, 
        /// its children, as well as the attached attribute nodes if there 
        /// are any. If the source node has a parent it is first removed 
        /// from the child list of its parent. This effectively allows moving
        /// a subtree from one document to another (unlike importNode() which
        /// create a copy of the source node instead of moving it). When it 
        /// fails, applications should use Document.importNode() instead. Note
        /// that if the adopted node is already part of this document (i.e. 
        /// the source and target document are the same), this method still 
        /// has the effect of removing the source node from the child list of 
        /// its parent, if any. The following list describes the specifics 
        /// for each type of node. 
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        public Node adoptNode(Node source)
        {
            if (source.nodeType == DOCUMENT_NODE || source.nodeType == DOCUMENT_TYPE_NODE)
                throw new DOMException(DOMException.NOT_SUPPORTED_ERR);

            if (source.nodeType == ENTITY_NODE || source.nodeType == NOTATION_NODE)
                return null;

            source.detachFromDocument();

            switch (source.nodeType)
            {
                case ATTRIBUTE_NODE:
                    break;
            }
            source.attachToDocument(this);

            return source;
        }

        /// <summary>
        /// 
        /// </summary>
        public string documentURI
        {
            get
            {
                return _documentURI != null ? _documentURI.Value : null;
            }
            set
            {
                _documentURI = new StringLookup(value);
            }
        }
        internal StringLookup _documentURI;

        public virtual IDOMConfiguration domConfig
        {
            get
            {
                return null;
            }
        }

        /// <summary>
        /// An attribute specifying the encoding used for this document at the
        /// time of the parsing. This is null when it is not known, such as 
        /// when the Document was created in memory.
        /// </summary>
        public string inputEncoding
        {
            get
            {
                return _inputEncoding != null ? _inputEncoding.Value : null;
            }
            internal set
            {
                _inputEncoding = new StringLookup(value);
            }
        }
        internal StringLookup _inputEncoding;

        /// <summary>
        /// This method acts as if the document was going through a save and 
        /// load cycle, putting the document in a "normal" form. As a 
        /// consequence, this method updates the replacement tree of 
        /// EntityReference nodes and normalizes Text nodes, as defined in 
        /// the method Node.normalize(). 
        /// </summary>
        public void normalizeDocument()
        {
            foreach (Node n in Children)
            {
                n.normalize();
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public object onload { get; set; }

        /// <summary>
        /// Rename an existing node of type ELEMENT_NODE or ATTRIBUTE_NODE.
        /// 
        /// When possible this simply changes the name of the given node, 
        /// otherwise this creates a new node with the specified name and 
        /// replaces the existing node with the new node as described below.
        /// 
        /// If simply changing the name of the given node is not possible, the 
        /// following operations are performed: a new node is created, any 
        /// registered event listener is registered on the new node, any user 
        /// data attached to the old node is removed from that node, the old 
        /// node is removed from its parent if it has one, the children are 
        /// moved to the new node, if the renamed node is an Element its 
        /// attributes are moved to the new node, the new node is inserted at 
        /// the position the old node used to have in its parent's child nodes
        /// list if it has one, the user data that was attached to the old node
        /// is attached to the new node.
        /// 
        /// When the node being renamed is an Element only the specified 
        /// attributes are moved, default attributes originated from the DTD are
        /// updated according to the new element name. In addition, the 
        /// implementation may update default attributes from other schemas. 
        /// Applications should use Document.normalizeDocument() to guarantee 
        /// these attributes are up-to-date.
        /// 
        /// When the node being renamed is an Attr that is attached to an Element,
        /// the node is first removed from the Element attributes map. Then, once 
        /// renamed, either by modifying the existing node or creating a new one 
        /// as described above, it is put back.
        /// 
        /// In addition,
        /// a user data event NODE_RENAMED is fired,
        /// when the implementation supports the feature "MutationNameEvents",
        /// each mutation operation involved in this method fires the appropriate 
        /// event, and in the end the event 
        /// {http://www.w3.org/2001/xml-events, DOMElementNameChanged} or 
        /// {http://www.w3.org/2001/xml-events, DOMAttributeNameChanged} is fired.
        /// </summary>
        /// <param name="n"></param>
        /// <param name="namespaceURI"></param>
        /// <param name="qualifiedName"></param>
        /// <returns></returns>
        public Node renameNode(Node n, string namespaceURI, string qualifiedName)
        {
            if ((n as Element) != null)
            {
                Element e = n as Element;
                e.namespaceURI = namespaceURI;
                e.TagName = qualifiedName;
            }
            else if ((n as Attr) != null)
            {
                Attr a = n as Attr;
                a.namespaceURI = namespaceURI;
                a.Name = qualifiedName;
                a.Specified = true;
            }
            return n;
        }

        /// <summary>
        /// An attribute specifying whether error checking is enforced or not.
        /// When set to false, the implementation is free to not test every 
        /// possible error case normally defined on DOM operations, and not 
        /// raise any DOMException on DOM operations or report errors while 
        /// using Document.normalizeDocument(). In case of error, the behavior
        /// is undefined. This attribute is true by default.
        /// </summary>
        public bool strictErrorChecking { get; set; }

        /// <summary>
        /// An attribute specifying, as part of the XML declaration, the 
        /// encoding of this document. This is null when unspecified or when 
        /// it is not known, such as when the Document was created in memory.
        /// </summary>
        public string xmlEncoding
        {
            get
            {
                return _xmlEncoding != null ? _xmlEncoding.Value : null;
            }
            internal set
            {
                _xmlEncoding = new StringLookup(value);
            }
        }
        internal StringLookup _xmlEncoding;

        /// <summary>
        /// An attribute specifying, as part of the XML declaration, whether 
        /// this document is standalone. This is false when unspecified.
        /// </summary>
        public bool xmlStandalone { get; set; }

        /// <summary>
        /// An attribute specifying, as part of the XML declaration, the 
        /// version number of this document. If there is no declaration and if 
        /// this document supports the "XML" feature, the value is "1.0". If 
        /// this document does not support the "XML" feature, the value is 
        /// always null. Changing this attribute will affect methods that check
        /// for invalid characters in XML names. Application should invoke 
        /// Document.normalizeDocument() in order to check for invalid 
        /// characters in the Nodes that are already part of this Document.
        /// 
        /// DOM applications may use the DOMImplementation.hasFeature(feature,
        /// version) method with parameter values "XMLVersion" and "1.0" 
        /// (respectively) to determine if an implementation supports [XML 1.0].
        /// DOM applications may use the same method with parameter values 
        /// "XMLVersion" and "1.1" (respectively) to determine if an 
        /// implementation supports [XML 1.1]. In both cases, in order to 
        /// support XML, an implementation must also support the "XML" feature
        /// defined in this specification. Document objects supporting a 
        /// version of the "XMLVersion" feature must not raise a 
        /// NOT_SUPPORTED_ERR exception for the same version number when using
        /// Document.xmlVersion. 
        /// </summary>
        public string xmlVersion
        {
            get
            {
                return _xmlVersion != null ? _xmlVersion.Value : null;
            }
            set
            {
                _xmlVersion = new StringLookup(value);
            }
        }
        internal StringLookup _xmlVersion;

        #endregion

        #region Custom DOM

        internal RequestContext Context { get; set; }

        #endregion

        #region IDocumentView Members

        /// <summary>
        /// 
        /// </summary>
        public IAbstractView defaultView
        {
            get { return Context.Window; }
        }

        #endregion
    }
}
